<?php

namespace Ysian\Alipaysdk;

use Closure;
use Ysian\Alipaysdk\tools\Attachment;
use Ysian\Alipaysdk\tools\Str;

class AlipayClient
{
    private $gatewayUrl; //支付宝网关
    private $appId; //应用appid
    private $rsaPrivateKey; //开发者私钥
    private $alipayrsaPublicKey; //支付宝公钥
    private $apiVersion; //api版本号
    private $signType; //加密类型
    private $postCharset; //编码
    private $format; //格式
    private $bizContent; //参数
    private $apiParam; //参数
    private $appAuthToken; //三方授权
    private $authToken; //用户授权
    private $log; //每个框架都有不同的日志记录方式,此为日志记录匿名函数
    private $deleteArr; //远程文件下载本地,支付宝读取后,删除文件集合
    private static $instance;

    /**
     * @初始化
     * @return self
     */
    public static function create()
    {
        if (empty(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * @param array $param
     * @return $this
     * @throws \Exception
     */
    public function setAop(array $param)
    {
        try {
            $this->gatewayUrl           =   isset($param['gatewayUrl']) ? $param['gatewayUrl'].'?charset=utf-8':'https://openapi.alipay.com/gateway.do?charset=utf-8';
            $this->appId                =   $param['appId'];
            $this->rsaPrivateKey        =   $param['rsaPrivateKey'];
            $this->alipayrsaPublicKey   =   $param['alipayrsaPublicKey'];
            $this->apiVersion           =   isset($param['apiVersion']) ? $param['apiVersion']:'1.0';
            $this->signType             =   isset($param['signType']) ? $param['signType']:'RSA2';
            $this->postCharset          =   'utf-8';
            $this->format               =   isset($param['format']) ? $param['format'] : 'json';
        } catch (\Exception $e){
            $msg = substr($e->getMessage(),21,-1);
            throw new \Exception( '缺少基础参数'.$msg);
        }
        return $this;
    }

    /**
     * @desc 设置api参数
     * @param array $bizContent
     * @return $this
     */
    public function setBizContent( array $bizContent)
    {
        $this->bizContent = json_encode($bizContent);
        return $this;
    }

    /**
     * @desc 设置参数
     * @param array $apiParam
     * @return $this
     * @throws \Exception
     */
    public function setRequest(array $apiParam)
    {
        $this->apiParam = $apiParam;
        return $this;
    }

    /**
     * @desc 设置appAuthToken
     * @param $appAuthToken
     * @return $this
     */
    public function setAppAuthToken($appAuthToken)
    {
        $this->appAuthToken = $appAuthToken;
        return $this;
    }

    /**
     * @desc 设置authToken
     * @param $authToken
     * @return $this
     */
    public function setAuthToken($authToken)
    {
        $this->authToken = $authToken;
        return $this;
    }

    /**
     * @desc 记录参数和输出结果
     * @param Closure $callback
     * @return $this
     */
    public function setLog(Closure $callback)
    {
        $this->log = $callback;
        return $this;
    }

    /**
     * @desc 组装参数,接口名称必填
     * @param $method
     * @return array
     * @throws \Exception
     */
    private function getParams($method)
    {
        if(empty($method)) throw new \Exception('请设置接口名称');
        #基础参数
        $param = [
            'app_id'    => $this->appId,
            'format'    => $this->format,
            'charset'   => $this->postCharset,
            'sign_type' => $this->signType,
            'timestamp' => date('Y-m-d H:i:s'),
            'version'   => $this->apiVersion,
            'method'    => $method,
        ];

        # 通过setRequest传来的参数
        $upload = [];
        $deleteArr = [];
        if ($this->apiParam) {
            foreach ($this->apiParam as $k=>$v) { //文件上传(图片,视频)不参与加签,
                if(is_array($v)) {
                    $param[$k] = json_encode($v);
                }elseif ('@' == substr($v, 0, 1)) { //文件上传
                    if ('https://'===substr($v, 0, 8) || 'http://'===substr($v, 0, 7)) {
                        $v = Attachment::download($v);
                        $deleteArr[] = $v;
                    }
                    $upload[$k] = new \CURLFile(substr($v, 1));
                } else {
                    $param[$k] = $v;
                }
            }
        }
        $this->deleteArr = $deleteArr;

        # 通过bizContent传来的参数
        if($this->bizContent) $param['biz_content'] = $this->bizContent;

        # app_auth_token 商户应用授权令牌
        if($this->appAuthToken)  $param['app_auth_token'] = $this->appAuthToken;

        # auth_token 用户授权令牌
        if($this->authToken)  $param['auth_token'] = $this->authToken;

        # 签名
        $param['sign'] = $this->getSign($param);
        $params = array_merge($param,$upload);

        # 参数回归初始化状态,防止下个请求携带这上个请求数据,导致参数污染
        $this->apiParam = '';
        $this->bizContent = '';
        $this->appAuthToken = '';

        return $params;
    }

    /**
     * @desc 获取数据接口
     * @return mixed
     * @throws \Exception
     */
    public function execute($method)
    {
        #1 组合接口所需参数
        $params = $this->getParams($method);
        #2 发送请求,获取所需数据
        $output = $this->curl_post($this->gatewayUrl,$params);
        #3 解析数据
        $res = json_decode($output, true);
        if(isset($res['error_response'])) throw new \Exception($res['error_response']['sub_msg']);
        $responseNode = str_replace(".", "_", $method) . "_response";
        $data = $res[$responseNode];
        $sign = $res['sign'];
        #5 验签并返回数据
        $this->verifySign($data,$sign,$this->alipayrsaPublicKey,$this->signType);
        if(isset($data['code']) && $data['code'] !== '10000') throw new \Exception($data['sub_msg']);
        return $data;
    }

    /**
     * @desc 私钥加签
     * @param $params
     * @return string
     * @throws \Exception
     */
    private function getSign($params)
    {
        #1 获取私钥
        if (file_exists($this->rsaPrivateKey)) {
            $content = file_get_contents($this->rsaPrivateKey);
        } else {
            $content = "-----BEGIN RSA PRIVATE KEY-----\n" .
                wordwrap($this->rsaPrivateKey, 64, "\n", true) .
                "\n-----END RSA PRIVATE KEY-----\n";
        }
        #2 获取私钥key
        $priKey = openssl_get_privatekey($content);
        if ($priKey === false) {
            throw new \Exception('证书文件路径错误或者传入的证书格式错误');
        }
        #3 加密
        $str = Str::getSignStr($params);
        $this->signType = 'RSA2' ? openssl_sign($str, $sign, $priKey, OPENSSL_ALGO_SHA256):openssl_sign($str, $sign, $priKey);
        #4 返回加签数据
        return base64_encode($sign);
    }

    /**
     * @desc 验签
     * @param $data 验签数据
     * @param $sign 签名
     * @param $rsaPublicKeyFilePath 公钥
     * @param string $signType 签名方式
     * @return bool
     * @throws \Exception
     */
    private function verifySign($data, $sign, $rsaPublicKeyFilePath, $signType = 'RSA')
    {
        #1 获取公钥
        try {
            $content = file_get_contents($rsaPublicKeyFilePath);
        } catch (\Exception $exception) {
            $content = "-----BEGIN PUBLIC KEY-----\n" .
                wordwrap($rsaPublicKeyFilePath, 64, "\n", true) .
                "\n-----END PUBLIC KEY-----\n";
        }

        #2 获取key
        $key = openssl_get_publickey($content);
        if ($key === false) throw new \Exception('秘钥文件路径错误或者传入的秘钥格式错误');

        #3 处理验签数据
        if(is_array($data)) $data = json_encode($data,JSON_UNESCAPED_UNICODE);
        if($signType=='RSA2') {
            $ret = openssl_verify($data, base64_decode($sign,true), $key, OPENSSL_ALGO_SHA256);
        } else {
            $ret = openssl_verify($data, base64_decode($sign,true), $key);
        }

        #4 返回验签结果
        if ($ret!==1) throw new \Exception( '验签失败');
        return true;
    }

    /**
     * @desc post请求
     * @param  $url
     * @param array $data
     * @return bool|string
     */
    private function curl_post( $url , $data=[])
    {
        $ch = curl_init(); //创建一个新cURL资源
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); //忽略证书验证
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($ch, CURLOPT_POST, 1); // POST数据
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // 把post的变量加上
        $output = curl_exec($ch); // 抓取URL并把它传递给浏览器

        //支付宝读取后删除上传到服务器的文件
        foreach ($this->deleteArr as $v){
            unlink($v);
        }

        if($this->log) {  //记录信息
            $record = '【curl_err_no:'.curl_errno($ch).'】  【param:'.json_encode($data).'】  【result:'.json_encode($output).'】';
            call_user_func($this->log,$record);
        }

        if (curl_errno($ch)) {
            throw new \Exception(curl_error($ch), 0);
        } else {
            $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if (200 !== $httpStatusCode) {
                throw new \Exception($output, $httpStatusCode);
            }
        }
        curl_close($ch); // 关闭cURL资源，并且释放系统资源
        return $output;
    }
}
